# Read data, I choose African Lions as the animal
animal <- read.csv('~/Downloads/African Lion - Panthera Lion Central Kalahari Game Reserve.csv') # name the file as animal
head(animal)
## event.id visible timestamp location.long location.lat
## 1 33042889052 true 2008-12-15 05:00:00.000 23.65028 -21.22511
## 2 33042889053 true 2008-12-15 06:00:00.000 23.65590 -21.22162
## 3 33042889054 true 2008-12-15 07:00:00.000 23.65950 -21.22039
## 4 33042889055 true 2008-12-15 08:00:00.000 23.65954 -21.22042
## 5 33042889056 true 2008-12-15 09:00:00.000 23.65981 -21.22146
## 6 33042889057 true 2008-12-15 10:00:00.000 23.65984 -21.22146
## eobs.temperature gps.dop height.raw sensor.type
## 1 29 1.8 987.00 gps
## 2 30 2.0 983.90 gps
## 3 31 1.8 978.80 gps
## 4 33 3.4 990.30 gps
## 5 35 2.2 987.63 gps
## 6 36 3.6 987.52 gps
## individual.taxon.canonical.name tag.local.identifier
## 1 Panthera leo GSM05751
## 2 Panthera leo GSM05751
## 3 Panthera leo GSM05751
## 4 Panthera leo GSM05751
## 5 Panthera leo GSM05751
## 6 Panthera leo GSM05751
## individual.local.identifier
## 1 1003
## 2 1003
## 3 1003
## 4 1003
## 5 1003
## 6 1003
## study.name
## 1 African Lion - Panthera Lion Central Kalahari Game Reserve
## 2 African Lion - Panthera Lion Central Kalahari Game Reserve
## 3 African Lion - Panthera Lion Central Kalahari Game Reserve
## 4 African Lion - Panthera Lion Central Kalahari Game Reserve
## 5 African Lion - Panthera Lion Central Kalahari Game Reserve
## 6 African Lion - Panthera Lion Central Kalahari Game Reserve
# check what variable are available
names(animal)
## [1] "event.id" "visible"
## [3] "timestamp" "location.long"
## [5] "location.lat" "eobs.temperature"
## [7] "gps.dop" "height.raw"
## [9] "sensor.type" "individual.taxon.canonical.name"
## [11] "tag.local.identifier" "individual.local.identifier"
## [13] "study.name"
# Unique individuals
individuals <- unique(animal$individual.local.identifier)
individuals
## [1] 1003 1001 1002 1013 1008 1012 1006 1007 1005 1004 1010 1009 1011
We have 13 individuals in the data.
animal_sf <- st_as_sf(animal, coords = c("location.long", "location.lat"), crs = 4326)
# Switch to interactive mode
tmap_mode("plot") # for your final maps you can use static maps
## ℹ tmap mode set to "plot".
# Basic map with OpenStreetMap + African Lions points
tm_shape(animal_sf) +
tm_basemap(server = "OpenStreetMap") +
tm_symbols(
col = "individual.local.identifier",
palette = "Set1",
size = 0.5,
border.col = "black", # set outline colour
border.lwd = 1 # set outline thickness (1 is default)
) +
tm_view(set.view = c(-22, 17, 10)) # Center + zoom
##
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_symbols()`: migrate the argument(s) related to the scale of the
## visual variable `fill` namely 'palette' (rename to 'values') to fill.scale =
## tm_scale(<HERE>).[v3->v4] `symbols()`: use 'fill' for the fill color of polygons/symbols
## (instead of 'col'), and 'col' for the outlines (instead of 'border.col').[v3->v4] `tm_view()`: use set_view instead of set.view[cols4all] color palettes: use palettes from the R package cols4all. Run
## `cols4all::c4a_gui()` to explore them. The old palette name "Set1" is named
## "brewer.set1"
Prepare the data:
# Check which rows have NA values in coordinates
NAPoints <- which(is.na(animal$location.lat) | is.na(animal$location.long))
# First we need to convert timestamp from factor to datetime
animal$tstamp <- strptime(as.character(animal$timestamp), "%Y-%m-%d %H:%M:%S")
# Then we separate timestamp into individual components
animal$date <- as.Date(animal$tstamp)
animal$year <- year(animal$tstamp)
animal$month <- month(animal$tstamp)
animal$day <- day(animal$tstamp)
animal$hour <- hour(animal$tstamp)
animal$min <- minute(animal$tstamp)
animal$sec <- second(animal$tstamp)
head(animal)
## event.id visible timestamp location.long location.lat
## 1 33042889052 true 2008-12-15 05:00:00.000 23.65028 -21.22511
## 2 33042889053 true 2008-12-15 06:00:00.000 23.65590 -21.22162
## 3 33042889054 true 2008-12-15 07:00:00.000 23.65950 -21.22039
## 4 33042889055 true 2008-12-15 08:00:00.000 23.65954 -21.22042
## 5 33042889056 true 2008-12-15 09:00:00.000 23.65981 -21.22146
## 6 33042889057 true 2008-12-15 10:00:00.000 23.65984 -21.22146
## eobs.temperature gps.dop height.raw sensor.type
## 1 29 1.8 987.00 gps
## 2 30 2.0 983.90 gps
## 3 31 1.8 978.80 gps
## 4 33 3.4 990.30 gps
## 5 35 2.2 987.63 gps
## 6 36 3.6 987.52 gps
## individual.taxon.canonical.name tag.local.identifier
## 1 Panthera leo GSM05751
## 2 Panthera leo GSM05751
## 3 Panthera leo GSM05751
## 4 Panthera leo GSM05751
## 5 Panthera leo GSM05751
## 6 Panthera leo GSM05751
## individual.local.identifier
## 1 1003
## 2 1003
## 3 1003
## 4 1003
## 5 1003
## 6 1003
## study.name
## 1 African Lion - Panthera Lion Central Kalahari Game Reserve
## 2 African Lion - Panthera Lion Central Kalahari Game Reserve
## 3 African Lion - Panthera Lion Central Kalahari Game Reserve
## 4 African Lion - Panthera Lion Central Kalahari Game Reserve
## 5 African Lion - Panthera Lion Central Kalahari Game Reserve
## 6 African Lion - Panthera Lion Central Kalahari Game Reserve
## tstamp date year month day hour min sec
## 1 2008-12-15 05:00:00 2008-12-15 2008 12 15 5 0 0
## 2 2008-12-15 06:00:00 2008-12-15 2008 12 15 6 0 0
## 3 2008-12-15 07:00:00 2008-12-15 2008 12 15 7 0 0
## 4 2008-12-15 08:00:00 2008-12-15 2008 12 15 8 0 0
## 5 2008-12-15 09:00:00 2008-12-15 2008 12 15 9 0 0
## 6 2008-12-15 10:00:00 2008-12-15 2008 12 15 10 0 0
# What years do we have?
years <- unique(animal$year)
#Create a birdyear id for so that trajectories of different years can be separated if we so wish.
animal$idyear<-paste0(animal$individual.local.identifier, year(animal$date))
# Get individuals by year
individualYear <- unique(animal$idyear)
individualYear
## [1] "10032008" "10032009" "10012009" "10012010" "10012011" "10022011"
## [7] "10022012" "10132010" "10132011" "10132012" "10082010" "10082011"
## [13] "10122010" "10122011" "10062010" "10062011" "10072011" "10052011"
## [19] "10052012" "10042011" "10042012" "10022009" "10022010" "10102009"
## [25] "10122009" "10092009" "10092010" "10112009" "10112010" "10072010"
## [31] "10042010" "10092011" "10112011"
The time period from 2008 to 2011 in the data.
# Convert timestamp to POSIX format
animal$POSIX <- as.POSIXct(animal$timestamp, format="%Y-%m-%d %H:%M:%S")
# Are any times duplicated?
duplics <- which(duplicated(animal$POSIX)==TRUE)
# Yes, we have a few points like this, so let's remove them.
animal <- animal[-duplics,]
# Convert the ducks data frame into an sf object by specifying which columns are coordinates
# Also we need to specify coordinate system, which in our case is WGS1984 (EPSG:4326 - for EPSG codes we just need the number))
animalSF <- st_as_sf(animal, coords=c('location.long','location.lat'), crs=4326)
animalSF
## Simple feature collection with 41614 features and 21 fields
## Geometry type: POINT
## Dimension: XY
## Bounding box: xmin: 22.37396 ymin: -21.90794 xmax: 23.95741 ymax: -20.84132
## Geodetic CRS: WGS 84
## First 10 features:
## event.id visible timestamp eobs.temperature gps.dop
## 1 33042889052 true 2008-12-15 05:00:00.000 29 1.8
## 2 33042889053 true 2008-12-15 06:00:00.000 30 2.0
## 3 33042889054 true 2008-12-15 07:00:00.000 31 1.8
## 4 33042889055 true 2008-12-15 08:00:00.000 33 3.4
## 5 33042889056 true 2008-12-15 09:00:00.000 35 2.2
## 6 33042889057 true 2008-12-15 10:00:00.000 36 3.6
## 7 33042889058 true 2008-12-15 12:00:00.000 34 2.2
## 8 33042889059 true 2008-12-15 13:00:00.000 34 2.2
## 9 33042889060 true 2008-12-15 14:00:00.000 34 2.0
## 10 33042889061 true 2008-12-15 16:00:00.000 33 1.6
## height.raw sensor.type individual.taxon.canonical.name tag.local.identifier
## 1 987.00 gps Panthera leo GSM05751
## 2 983.90 gps Panthera leo GSM05751
## 3 978.80 gps Panthera leo GSM05751
## 4 990.30 gps Panthera leo GSM05751
## 5 987.63 gps Panthera leo GSM05751
## 6 987.52 gps Panthera leo GSM05751
## 7 986.41 gps Panthera leo GSM05751
## 8 986.90 gps Panthera leo GSM05751
## 9 986.08 gps Panthera leo GSM05751
## 10 985.46 gps Panthera leo GSM05751
## individual.local.identifier
## 1 1003
## 2 1003
## 3 1003
## 4 1003
## 5 1003
## 6 1003
## 7 1003
## 8 1003
## 9 1003
## 10 1003
## study.name
## 1 African Lion - Panthera Lion Central Kalahari Game Reserve
## 2 African Lion - Panthera Lion Central Kalahari Game Reserve
## 3 African Lion - Panthera Lion Central Kalahari Game Reserve
## 4 African Lion - Panthera Lion Central Kalahari Game Reserve
## 5 African Lion - Panthera Lion Central Kalahari Game Reserve
## 6 African Lion - Panthera Lion Central Kalahari Game Reserve
## 7 African Lion - Panthera Lion Central Kalahari Game Reserve
## 8 African Lion - Panthera Lion Central Kalahari Game Reserve
## 9 African Lion - Panthera Lion Central Kalahari Game Reserve
## 10 African Lion - Panthera Lion Central Kalahari Game Reserve
## tstamp date year month day hour min sec idyear
## 1 2008-12-15 05:00:00 2008-12-15 2008 12 15 5 0 0 10032008
## 2 2008-12-15 06:00:00 2008-12-15 2008 12 15 6 0 0 10032008
## 3 2008-12-15 07:00:00 2008-12-15 2008 12 15 7 0 0 10032008
## 4 2008-12-15 08:00:00 2008-12-15 2008 12 15 8 0 0 10032008
## 5 2008-12-15 09:00:00 2008-12-15 2008 12 15 9 0 0 10032008
## 6 2008-12-15 10:00:00 2008-12-15 2008 12 15 10 0 0 10032008
## 7 2008-12-15 12:00:00 2008-12-15 2008 12 15 12 0 0 10032008
## 8 2008-12-15 13:00:00 2008-12-15 2008 12 15 13 0 0 10032008
## 9 2008-12-15 14:00:00 2008-12-15 2008 12 15 14 0 0 10032008
## 10 2008-12-15 16:00:00 2008-12-15 2008 12 15 16 0 0 10032008
## POSIX geometry
## 1 2008-12-15 05:00:00 POINT (23.65028 -21.22511)
## 2 2008-12-15 06:00:00 POINT (23.6559 -21.22162)
## 3 2008-12-15 07:00:00 POINT (23.6595 -21.22039)
## 4 2008-12-15 08:00:00 POINT (23.65954 -21.22042)
## 5 2008-12-15 09:00:00 POINT (23.65981 -21.22146)
## 6 2008-12-15 10:00:00 POINT (23.65984 -21.22146)
## 7 2008-12-15 12:00:00 POINT (23.65982 -21.22146)
## 8 2008-12-15 13:00:00 POINT (23.65981 -21.22145)
## 9 2008-12-15 14:00:00 POINT (23.65987 -21.22147)
## 10 2008-12-15 16:00:00 POINT (23.65985 -21.22145)
# Project data
# Define new projection
crsnew <- st_crs("ESRI:102014")
# Transform data into the new coordinate system
animalSF_proj <- st_transform(animalSF,crs=crsnew)
head(animalSF_proj)
## Simple feature collection with 6 features and 21 fields
## Geometry type: POINT
## Dimension: XY
## Bounding box: xmin: 2891248 ymin: -7700646 xmax: 2893092 ymax: -7699222
## Projected CRS: Europe_Lambert_Conformal_Conic
## event.id visible timestamp eobs.temperature gps.dop
## 1 33042889052 true 2008-12-15 05:00:00.000 29 1.8
## 2 33042889053 true 2008-12-15 06:00:00.000 30 2.0
## 3 33042889054 true 2008-12-15 07:00:00.000 31 1.8
## 4 33042889055 true 2008-12-15 08:00:00.000 33 3.4
## 5 33042889056 true 2008-12-15 09:00:00.000 35 2.2
## 6 33042889057 true 2008-12-15 10:00:00.000 36 3.6
## height.raw sensor.type individual.taxon.canonical.name tag.local.identifier
## 1 987.00 gps Panthera leo GSM05751
## 2 983.90 gps Panthera leo GSM05751
## 3 978.80 gps Panthera leo GSM05751
## 4 990.30 gps Panthera leo GSM05751
## 5 987.63 gps Panthera leo GSM05751
## 6 987.52 gps Panthera leo GSM05751
## individual.local.identifier
## 1 1003
## 2 1003
## 3 1003
## 4 1003
## 5 1003
## 6 1003
## study.name
## 1 African Lion - Panthera Lion Central Kalahari Game Reserve
## 2 African Lion - Panthera Lion Central Kalahari Game Reserve
## 3 African Lion - Panthera Lion Central Kalahari Game Reserve
## 4 African Lion - Panthera Lion Central Kalahari Game Reserve
## 5 African Lion - Panthera Lion Central Kalahari Game Reserve
## 6 African Lion - Panthera Lion Central Kalahari Game Reserve
## tstamp date year month day hour min sec idyear
## 1 2008-12-15 05:00:00 2008-12-15 2008 12 15 5 0 0 10032008
## 2 2008-12-15 06:00:00 2008-12-15 2008 12 15 6 0 0 10032008
## 3 2008-12-15 07:00:00 2008-12-15 2008 12 15 7 0 0 10032008
## 4 2008-12-15 08:00:00 2008-12-15 2008 12 15 8 0 0 10032008
## 5 2008-12-15 09:00:00 2008-12-15 2008 12 15 9 0 0 10032008
## 6 2008-12-15 10:00:00 2008-12-15 2008 12 15 10 0 0 10032008
## POSIX geometry
## 1 2008-12-15 05:00:00 POINT (2891248 -7700646)
## 2 2008-12-15 06:00:00 POINT (2892276 -7699642)
## 3 2008-12-15 07:00:00 POINT (2892975 -7699222)
## 4 2008-12-15 08:00:00 POINT (2892986 -7699227)
## 5 2008-12-15 09:00:00 POINT (2893087 -7699449)
## 6 2008-12-15 10:00:00 POINT (2893092 -7699448)
# Read X an Y from geometry column as separate coordinates
animalSF_proj$X <- st_coordinates(animalSF_proj)[,1]
animalSF_proj$Y <- st_coordinates(animalSF_proj)[,2]
# Drop geometry column
animalSF_proj <- st_drop_geometry(animalSF_proj)
head(animalSF_proj)
## event.id visible timestamp eobs.temperature gps.dop
## 1 33042889052 true 2008-12-15 05:00:00.000 29 1.8
## 2 33042889053 true 2008-12-15 06:00:00.000 30 2.0
## 3 33042889054 true 2008-12-15 07:00:00.000 31 1.8
## 4 33042889055 true 2008-12-15 08:00:00.000 33 3.4
## 5 33042889056 true 2008-12-15 09:00:00.000 35 2.2
## 6 33042889057 true 2008-12-15 10:00:00.000 36 3.6
## height.raw sensor.type individual.taxon.canonical.name tag.local.identifier
## 1 987.00 gps Panthera leo GSM05751
## 2 983.90 gps Panthera leo GSM05751
## 3 978.80 gps Panthera leo GSM05751
## 4 990.30 gps Panthera leo GSM05751
## 5 987.63 gps Panthera leo GSM05751
## 6 987.52 gps Panthera leo GSM05751
## individual.local.identifier
## 1 1003
## 2 1003
## 3 1003
## 4 1003
## 5 1003
## 6 1003
## study.name
## 1 African Lion - Panthera Lion Central Kalahari Game Reserve
## 2 African Lion - Panthera Lion Central Kalahari Game Reserve
## 3 African Lion - Panthera Lion Central Kalahari Game Reserve
## 4 African Lion - Panthera Lion Central Kalahari Game Reserve
## 5 African Lion - Panthera Lion Central Kalahari Game Reserve
## 6 African Lion - Panthera Lion Central Kalahari Game Reserve
## tstamp date year month day hour min sec idyear
## 1 2008-12-15 05:00:00 2008-12-15 2008 12 15 5 0 0 10032008
## 2 2008-12-15 06:00:00 2008-12-15 2008 12 15 6 0 0 10032008
## 3 2008-12-15 07:00:00 2008-12-15 2008 12 15 7 0 0 10032008
## 4 2008-12-15 08:00:00 2008-12-15 2008 12 15 8 0 0 10032008
## 5 2008-12-15 09:00:00 2008-12-15 2008 12 15 9 0 0 10032008
## 6 2008-12-15 10:00:00 2008-12-15 2008 12 15 10 0 0 10032008
## POSIX X Y
## 1 2008-12-15 05:00:00 2891248 -7700646
## 2 2008-12-15 06:00:00 2892276 -7699642
## 3 2008-12-15 07:00:00 2892975 -7699222
## 4 2008-12-15 08:00:00 2892986 -7699227
## 5 2008-12-15 09:00:00 2893087 -7699449
## 6 2008-12-15 10:00:00 2893092 -7699448
# First turn the SF into a data frame
animal_cleanedDF <- as.data.frame(animalSF_proj)
# Now save this into a csv file, giving the name of the projection in the filename
write.csv(animal_cleanedDF, 'animal_CleanData_projected_ESRI102014.csv')
# Create a set of trajectories, one per each individual
animal_ltraj <- as.ltraj(xy=animalSF_proj[,c('X','Y')], # spatial Coordinates
date = animalSF_proj$POSIX, # timestamp in POSIX format, including date/time
id = animalSF_proj$individual.local.identifier) # individual IDs
# Let's plot these trajectories to see if it worked
plot(animal_ltraj)
# Let's focus on 10 African Lions for this analysis, I will choose the individual 1001 to 1010. Include the the long-distance traveller and sedentary.
animal1 <- animal_ltraj[[1]]
animal2 <- animal_ltraj[[2]]
animal3 <- animal_ltraj[[3]]
animal4 <- animal_ltraj[[4]]
animal5 <- animal_ltraj[[5]]
animal6 <- animal_ltraj[[6]]
animal7 <- animal_ltraj[[7]]
animal8 <- animal_ltraj[[8]]
animal9 <- animal_ltraj[[9]]
animal10 <- animal_ltraj[[10]]
animal_list <- list(animal1, animal2, animal3, animal4, animal5,
animal6, animal7, animal8, animal9, animal10)
for (i in 1:length(animal_list)) {
animal <- animal_list[[i]]
animal$speedMS <- animal$dist / animal$dt
animal$speedKmH <- animal$speedMS * 3.6
duration_plot <- ggplot(animal, aes(x=dt)) +
geom_histogram() +
theme_bw() +
xlab("Segment duration (s)") +
ylab("Count") +
ggtitle(paste("Segment Duration - Animal", i))
length_plot <- ggplot(animal, aes(x=dist)) +
geom_histogram() +
theme_bw() +
xlab("Segment length (m)") +
ylab("Count") +
ggtitle(paste("Segment Length - Animal", i))
speed_plot <- ggplot(animal, aes(x=speedKmH)) +
geom_histogram() +
theme_bw() +
xlab("Speed (km/h)") +
ylab("Count") +
ggtitle(paste("Speed - Animal", i))
grid.newpage()
pushViewport(viewport(layout = grid.layout(3, 1)))
print(duration_plot, vp = viewport(layout.pos.row = 1, layout.pos.col = 1))
print(length_plot, vp = viewport(layout.pos.row = 2, layout.pos.col = 1))
print(speed_plot, vp = viewport(layout.pos.row = 3, layout.pos.col = 1))
}
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 2 rows containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 2 rows containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
cleaned_list <- list()
for (i in 1:length(animal_list)) {
animal <- animal_list[[i]]
before_plot <- ggplot(animal, aes(x=dt)) +
geom_boxplot() +
ggtitle(paste0("Before cleaning: animal", i)) +
theme_minimal()
animal_cleaned <- animal[-which(animal$dt > 10000), ]
cleaned_list[[i]] <- animal_cleaned
after_plot <- ggplot(animal_cleaned, aes(x=dt)) +
geom_boxplot() +
ggtitle(paste0("After cleaning: animal", i)) +
theme_minimal()
grid.arrange(before_plot, after_plot, nrow = 2)
}
## Warning: Removed 2 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
## Removed 2 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
## Removed 1 row containing non-finite outside the scale range (`stat_boxplot()`).
n <- length(animal_list) # Number of individuals
# Initialize an empty data frame with columns for the statistics we want to calculate
results <- data.frame(
dt_mean = numeric(n),
dt_std = numeric(n),
dist_mean = numeric(n),
dist_std = numeric(n),
speedKmH_mean = numeric(n),
speedKmH_std = numeric(n)
)
# Step 3: For loop to calculate all the statistics
for (i in 1:length(animal_list)) {
# Here I have copied the calculation code from step 1, but edited to include the index i
# First we need to assign the data to a generic variable we will use in the for loop
thisanimal <- animal_ltraj[[i]] # note how this is the same as above, except I replaced 1 with the index i
# We also need to calculate speed, because we haven't done that for all ducks yet
thisanimal$speedKmH <- (thisanimal$dist/thisanimal$dt)*3.6
# Then we need to get rid of all NA values in the respective columns, using the two concepts you already know, is.na and | operator
dtNAs <- which(is.na(thisanimal$dt)==TRUE | is.na(thisanimal$dist)==TRUE | is.na(thisanimal$speedKmH)==TRUE)
# We remove rows with NAs
thisanimal <- thisanimal[-dtNAs,]
# Finally we calculate statistics for this duck, but unlike above, we now put these into the results data frame, in row i
results$dt_mean[i] <- mean(thisanimal$dt)
results$dt_std[i] <- sd(thisanimal$dt)
results$dist_mean[i] <- mean(thisanimal$dist)
results$dist_std[i] <- sd(thisanimal$dist)
results$speedKmH_mean[i] <- mean(thisanimal$speedKmH)
results$speedKmH_std[i] <- sd(thisanimal$speedKmH)
}
# We don't have to initialise the results data frame here, because we already have it. But we will add two columns to it, like this:
results$R2n_mean <- NA
results$R2n_std <- NA
# Now let's use a for loop to populate these two columns. The loop is very similar to the previous one:
for (i in 1:length(animal_list)) {
# Assign data to the generic variable
thisanimal <- animal_ltraj[[i]]
# Then we need to get rid of all NA values in the respective column
R2nNAs <- which(is.na(thisanimal$R2n)==TRUE)
# Here comes the new If statement: if there are some NAs, we remove them, if not, we don't do anything.
if (length(R2nNAs)>0) {
thisanimal <- thisanimal[-R2nNAs,]
}
# Finally we calculate statistics for this duck, but unlike above, we now put these into the results data frame, in row i
results$R2n_mean[i] <- mean(thisanimal$R2n)
results$R2n_std[i] <- sd(thisanimal$R2n)
}
for (i in 1:length(animal_list)) {
# Create the plot for each animal
plot <- ggplot(animal_ltraj[[i]], aes(x=date, y=R2n)) +
geom_point() +
theme_bw() +
xlab("Date") +
ylab("R2n") +
ggtitle(paste("Net square displacement over time for animal", i))
# Print the plot for each animal
print(plot)
}
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
# Take one duck
thisanimal <- animal_ltraj[[1]]
# Convert relative angle from radians into degrees
thisanimal$turningAngle <- thisanimal$rel.angle * 180 / pi
# Check it
head(thisanimal)
## x y date dx dy dist
## 1990 2703787 -7749541 2009-12-07 19:30:00 29.3199238 -56.009937 63.220020
## 1991 2703816 -7749597 2009-12-07 20:00:00 -6.4679256 -1.390088 6.615618
## 1992 2703810 -7749598 2009-12-07 20:30:00 0.3210739 -1.791937 1.820475
## 1993 2703810 -7749600 2009-12-07 21:00:00 -0.1821387 3.435114 3.439939
## 1994 2703810 -7749597 2009-12-07 21:30:00 -27.5632814 44.996767 52.767827
## 1995 2703782 -7749552 2009-12-07 22:00:00 112.9555376 -230.134102 256.360407
## dt R2n abs.angle rel.angle turningAngle
## 1990 1800 0.0000 -1.088544 NA NA
## 1991 1800 3996.7709 -2.929893 -1.8413489 -105.50152
## 1992 1800 3816.9766 -1.393501 1.5363920 88.02878
## 1993 1800 4040.6796 1.623769 3.0172700 172.87684
## 1994 1800 3637.4091 2.120401 0.4966316 28.45490
## 1995 1800 136.6857 -1.114516 3.0482686 174.65293
# Build a circular plot
angleplot <- ggplot(thisanimal, aes(x=turningAngle)) + geom_histogram(binwidth=2)+coord_polar(start = pi)+
theme_bw()+scale_x_continuous(limits = c(-180,180),breaks=c(-180,-90,0,90,180))+xlab("Turning Angle")+ylab("Count")+
ggtitle("Turning angle for animal 1")
# Export as a figure
png(filename="animal1_TurningAngle.png")
print(angleplot)
## Warning: Removed 19 rows containing non-finite outside the scale range
## (`stat_bin()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
dev.off()
## quartz_off_screen
## 2
# Remove NAs
turnNAs <- which(is.na(thisanimal$turningAngle)==TRUE)
# Here comes the new If statement: if there are some NAs, we remove them, if not, we don't do anything.
if (length(turnNAs)>0) {
thisanimal <- thisanimal[-turnNAs,]
}
# Calculate statistics - because angles are circular, we need to use circular mean and circular std, from package circular
turningAngle_mean <- mean(circular(thisanimal$turningAngle))
turningAngle_std <- sd(circular(thisanimal$turningAngle))
# Take a lion
thisanimal <- animal_ltraj[[2]]
# Check to see in which columns we have the x and y coordinates
head(thisanimal)
## x y date dx dy dist
## 80158 2870833 -7710273 2009-08-14 18:00:00 1.9234920 1.2928059 2.3175781
## 80159 2870835 -7710271 2009-08-14 19:00:00 -0.0852056 0.4467048 0.4547584
## 80160 2870835 -7710271 2009-08-14 20:00:00 -14.5114015 -9.2493346 17.2084562
## 80161 2870820 -7710280 2009-08-14 21:00:00 8.9965413 4.0308027 9.8582516
## 80162 2870829 -7710276 2009-08-14 22:00:00 -1.5971638 1.5471826 2.2236695
## 80163 2870828 -7710275 2009-08-14 23:00:00 2.4492185 -6.0142335 6.4938183
## dt R2n abs.angle rel.angle
## 80158 3600 0.000000 0.5917644 NA
## 80159 3600 5.371168 1.7592748 1.167510
## 80160 3600 6.405194 -2.5741375 1.949773
## 80161 3600 217.005304 0.4212221 2.995360
## 80162 3600 25.620784 2.3720888 1.950867
## 80163 3600 31.544310 -1.1840668 2.727030
# Ok, they are called x and y.
# So now we will take a loop that will go through this trajecotry. We need to know the number of points in the trajectory,
# we can just check the length of one of the coordinates.
nPoints <- length(thisanimal$x)
# Also we need to add a new field for the heading to thisduck data frame
thisanimal$heading <- NA
# For loop through the trajectory, but not to the end, we skip the final point!
for (i in 1:(nPoints-1)) {
# Let's read the coordinates from two consecutive points, i and i+1
x1 <- thisanimal$x[i]
x2 <- thisanimal$x[i+1]
y1 <- thisanimal$y[i]
y2 <- thisanimal$y[i+1]
# And calculate the heading as arctan of the two values, plus convert to degrees
thisanimal$heading[i] <- atan2((x2-x1),(y2-y1)) * 180 / pi
}
# Build a circular plot
angleplot <- ggplot(thisanimal, aes(x=heading)) + geom_histogram(binwidth=2)+coord_polar(start = pi)+
theme_bw()+scale_x_continuous(limits = c(-180,180),breaks=c(-180,-90,0,90,180))+
xlab("Heading")+ylab("Count")+ggtitle("Heading for duck 2")
# Export as a figure
png(filename="Animal2_Heading.png")
print(angleplot)
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
dev.off()
## quartz_off_screen
## 2
# Remove NAs - there will be at least one, for the last point through which we didn't loop
headingNAs <- which(is.na(thisanimal$heading)==TRUE)
# If there are some NAs, we remove them, if not, we don't do anything.
if (length(headingNAs)>0) {
thisanimal <- thisanimal[-headingNAs,]
}
# Calculate statistics - because angles are circular, we need to use circular mean and circular std, from package circular
heading_mean <- mean(circular(thisanimal$heading))
heading_std <- sd(circular(thisanimal$heading))
turning_summary <- data.frame(
animal_id = numeric(),
mean_angle = numeric(),
std_angle = numeric()
)
for (i in 1:length(animal_list)) {
animal <- animal_list[[i]]
if (!all(c("x", "y") %in% names(animal))) next
animal$turning <- NA
for (j in 2:(nrow(animal) - 1)) {
x1 <- animal$x[j - 1]
x2 <- animal$x[j]
x3 <- animal$x[j + 1]
y1 <- animal$y[j - 1]
y2 <- animal$y[j]
y3 <- animal$y[j + 1]
angle1 <- atan2(y2 - y1, x2 - x1)
angle2 <- atan2(y3 - y2, x3 - x2)
turn <- angle2 - angle1
if (turn > pi) turn <- turn - 2 * pi
if (turn < -pi) turn <- turn + 2 * pi
animal$turning[j] <- turn * 180 / pi
}
animal_cleaned <- animal[!is.na(animal$turning), ]
mean_turn <- mean(circular(animal_cleaned$turning, units = "degrees"))
sd_turn <- sd(circular(animal_cleaned$turning, units = "degrees"))
turning_summary <- rbind(turning_summary, data.frame(
animal_id = i,
mean_angle = mean_turn,
std_angle = sd_turn
))
angleplot <- ggplot(animal_cleaned, aes(x = turning)) +
geom_histogram(binwidth = 2) +
coord_polar(start = pi) +
theme_bw() +
scale_x_continuous(limits = c(-180, 180), breaks = c(-180, -90, 0, 90, 180)) +
xlab("Turning Angle") + ylab("Count") +
ggtitle(paste("Turning Angle for Animal", i))
print(angleplot)
cat(paste0("Animal ", i, " – Mean Turning Angle: ", round(mean_turn, 2),
"°, Std Dev: ", round(sd_turn, 2), "°\n\n"))
}
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 1 – Mean Turning Angle: -0.29°, Std Dev: 1.93°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 2 – Mean Turning Angle: -0.6°, Std Dev: 2.03°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 3 – Mean Turning Angle: 2.99°, Std Dev: 1.53°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 4 – Mean Turning Angle: 25.42°, Std Dev: 2.45°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 5 – Mean Turning Angle: -131.31°, Std Dev: 3.13°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 6 – Mean Turning Angle: 37.55°, Std Dev: 1.51°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 7 – Mean Turning Angle: 20.84°, Std Dev: 2.62°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 8 – Mean Turning Angle: 123.15°, Std Dev: 2.71°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 9 – Mean Turning Angle: -175.56°, Std Dev: 1.94°
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Animal 10 – Mean Turning Angle: 175.32°, Std Dev: 2.34°
# We don't have to initialise the results data frame here, because we already have it. But we will add two columns to it, like this:
results$heading_mean <- NA
results$heading_std <- NA
# Here start the nested loops
# External For loop for all the ducks - note the index is now called j!
for (j in 1:length(animal_list)) {
# Take a lion
thisanimal <- animal_ltraj[[j]]
# So now we will take a loop that will go through this trajecotry. We need to know the number of points in the trajectory,
# we can just check the length of one of the coordinates.
nPoints <- length(thisanimal$x)
# Also we need to add a new field for the heading to thisanimal data frame
thisanimal$heading <- NA
# Internal for loop through the trajectory, but not to the end, we skip the final point!
# This loop has index i
for (i in 1:(nPoints-1)) {
# Let's read the coordinates from two consecutive points, i and i+1
x1 <- thisanimal$x[i]
x2 <- thisanimal$x[i+1]
y1 <- thisanimal$y[i]
y2 <- thisanimal$y[i+1]
# And calculate the heading as arctan of the two values, plus convert to degrees
thisanimal$heading[i] <- atan2((x2-x1),(y2-y1)) * 180 / pi
} # end for i
# Build a circular plot
angleplot <- ggplot(thisanimal, aes(x=heading)) + geom_histogram(binwidth=2)+coord_polar(start = pi)+
theme_bw()+scale_x_continuous(limits = c(-180,180),breaks=c(-180,-90,0,90,180))+
xlab("Heading")+ylab("Count")+ggtitle(paste("Heading for animal ",j,sep=""))
# Export as a figure
#png(filename=paste("Animal",j,"_Heading.png",sep=""))
print(angleplot)
#dev.off()
# Remove NAs - there will be at least one, for the last point through which we didn't loop
headingNAs <- which(is.na(thisanimal$heading)==TRUE)
# Here comes the new If statement: if there are some NAs, we remove them, if not, we don't do anything.
if (length(headingNAs)>0) {
thisanimal <- thisanimal[-headingNAs,]
}
# Calculate statistics - because angles are circular, we need to use circular mean and circular std
results$heading_mean[j] <- mean(circular(thisanimal$heading))
results$heading_std[j] <- sd(circular(thisanimal$heading))
} # end for j
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_bin()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Warning: Removed 1 row containing non-finite outside the scale range (`stat_bin()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
# Check results
results
## dt_mean dt_std dist_mean dist_std speedKmH_mean speedKmH_std
## 1 2864.155 4921.787 736.2872 1270.044 1.3961524 2.3914207
## 2 7258.505 34250.186 962.3112 2922.488 0.9232217 1.6871668
## 3 6477.465 12728.197 1404.3166 2178.671 1.2778381 1.8743605
## 4 98387.025 330931.434 7274.8486 10273.839 0.3779828 0.6003488
## 5 10479.860 16850.670 1561.3494 4530.184 0.4102659 0.9691038
## 6 481693.043 1159510.388 12477.7770 22807.435 0.5326418 0.8773748
## 7 66831.111 174078.629 5539.7002 8103.403 0.4435427 0.7994241
## 8 28078.062 103455.111 2611.2533 6216.715 0.3440738 0.7046752
## 9 31443.061 96593.286 1408.3242 3627.827 0.2664249 0.7473899
## 10 19825.389 52526.566 2543.1237 6588.879 0.5157002 1.0516710
## R2n_mean R2n_std heading_mean heading_std
## 1 1246054400 2182639514 2.11934272 2.875035
## 2 1532098517 2013510350 -1.76883741 3.148480
## 3 1310816225 1097295839 -0.35291785 2.997561
## 4 3690232022 2715599705 0.90269072 2.693817
## 5 1082938101 1093634354 1.13997808 2.661001
## 6 1128149017 1169569646 -1.77421529 1.806196
## 7 450897353 546744742 2.27406183 2.813500
## 8 902467841 943075197 -0.01773315 2.439466
## 9 549330014 284666991 3.13593078 2.539468
## 10 1891362359 2542130782 -0.34558583 2.180880
# Prepare the field in the results data frame
results$gyration <- NA
# For loop across all ducks
for (i in 1:length(animal_list)) {
# Pick a duck
thisanimal <- animal_ltraj[[i]]
# Find number of points in this trajectory
noPoints <- length(thisanimal$x)
# Calculate the mean centre of all locations
centreX <- sum(thisanimal$x)/noPoints
centreY <- sum(thisanimal$y)/noPoints
# Distance of each point to (centreX, centreY)
thisanimal$distTocentre <- sqrt((centreX-thisanimal$x)^2+(centreY-thisanimal$y)^2)
# Calculate radius of gyration and insert it into the results data frame
results$gyration[i] <- sqrt(sum(thisanimal$distTocentre)/noPoints)
}
# Check results
results
## dt_mean dt_std dist_mean dist_std speedKmH_mean speedKmH_std
## 1 2864.155 4921.787 736.2872 1270.044 1.3961524 2.3914207
## 2 7258.505 34250.186 962.3112 2922.488 0.9232217 1.6871668
## 3 6477.465 12728.197 1404.3166 2178.671 1.2778381 1.8743605
## 4 98387.025 330931.434 7274.8486 10273.839 0.3779828 0.6003488
## 5 10479.860 16850.670 1561.3494 4530.184 0.4102659 0.9691038
## 6 481693.043 1159510.388 12477.7770 22807.435 0.5326418 0.8773748
## 7 66831.111 174078.629 5539.7002 8103.403 0.4435427 0.7994241
## 8 28078.062 103455.111 2611.2533 6216.715 0.3440738 0.7046752
## 9 31443.061 96593.286 1408.3242 3627.827 0.2664249 0.7473899
## 10 19825.389 52526.566 2543.1237 6588.879 0.5157002 1.0516710
## R2n_mean R2n_std heading_mean heading_std gyration
## 1 1246054400 2182639514 2.11934272 2.875035 147.7880
## 2 1532098517 2013510350 -1.76883741 3.148480 163.5427
## 3 1310816225 1097295839 -0.35291785 2.997561 139.6413
## 4 3690232022 2715599705 0.90269072 2.693817 188.2696
## 5 1082938101 1093634354 1.13997808 2.661001 145.4142
## 6 1128149017 1169569646 -1.77421529 1.806196 155.8137
## 7 450897353 546744742 2.27406183 2.813500 131.1873
## 8 902467841 943075197 -0.01773315 2.439466 159.2748
## 9 549330014 284666991 3.13593078 2.539468 95.2824
## 10 1891362359 2542130782 -0.34558583 2.180880 184.4834
# This is the last thing we will add to results, so let's also export these into a file.
write.csv(results, "Animal_results.csv", row.names=FALSE)
The dataset spans from 2008 to 2012, covering a multi-year period across multiple seasons. This range provides a robust foundation to explore both short-term behavioural rhythms and long-term patterns. Each individual has a slightly different data coverage, but the full temporal extent allows for comparative and time-based movement analytics.
Across the 10 individuals, movement behaviour diverges significantly in both spatial extent and temporal regularity. Based on net squared displacement (NSD), radius of gyration, trajectory maps, and heading distributions, we identify three broad behavioural categories:
Yes—several individuals stand out for their unique or potentially anomalous movement signatures:
Together, the turning angle plots reflect the tight vs. straight curves, while the heading plots indicate overall movement direction. For example, Animals 9 and 10 both show turning means close to ±180°, indicating repeated sharp reversals, which matches their dense, non-linear paths on the trajectory map.
In short, integrating turning angle and heading statistics with trajectory maps greatly enhances our ability to infer movement modes—whether directed travel, exploratory behaviour, or site fidelity.
These two spatial dispersion metrics offer distinct yet complementary insights:
By aligning spatial metrics with visual maps, we can validate movement interpretations and differentiate functional movement strategies—e.g., exploration, migration, residency.
This analysis assumes that the GPS data accurately reflects real movement, but data irregularities—such as long time gaps (Animal 6) or abnormally long segments (Animal 2)—can bias estimates of speed and displacement. Cleaning these outliers was essential, but limitations remain.
Future work could combine this movement data with environmental layers to model habitat preferences or movement constraints in a landscape ecology context.
Bastille-Rousseau, G., Potts, J.R., Yackulic, C.B. et al. Flexible characterization of animal movement pattern using net squared displacement and a latent state model. Mov Ecol 4, 15 (2016). https://doi.org/10.1186/s40462-016-0080-y
Codling, E.A., Plank, M.J., & Benhamou, S. (2008). Random walk models in biology. Journal of the Royal Society Interface, 5(25), 813–834. https://doi.org/10.1098/rsif.2008.0014
Nathan, R. et al. (2008). A movement ecology paradigm for unifying organismal movement research. Proceedings of the National Academy of Sciences, 105(49), 19052–19059. https://doi.org/10.1073/pnas.0800375105